﻿using System;
using System.Data;
using System.Linq;
using OverStore.Configuration.Validation;
using OverStore.Exceptions.Configuration;
using OverStore.Runtime.Logger;
using OverStore.Runtime.OverStoreContext;
using OverStore.Runtime.Storage;
using OverStore.Sql;
using OverStore.Sql.TableSchema;
using System.Diagnostics;
using OverStore.Runtime.ObjectRepository.CommandSet;

namespace OverStore.Configuration.ObjectRepository.CommandSet
{
    /// <summary>
    /// Configuration for <see cref="ICommandSet"/> component uses table schema for generate commands.
    /// </summary>
    public sealed class TableCommandSetConfiguration : IComponentConfiguration<ICommandSet>, ISupportValidation
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="TableCommandSetConfiguration"/> class.
        /// </summary>
        public TableCommandSetConfiguration()
        {
            TableSchema = new Table();
        }

        /// <summary>
        /// Gets or sets <c>true</c> if deleted object should be updated in data storage.
        /// </summary>
        public bool PerformDeleteAsUpdate
        {
            get;
            set;
        }

        #region Custom Commands

        /// <summary>
        /// Gets or sets the custom select command.
        /// </summary>
        public IComponentConfiguration<ICommand> CustomSelect
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the custom update command.
        /// </summary>
        public IComponentConfiguration<ICommand> CustomUpdate
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the custom insert command.
        /// </summary>
        public IComponentConfiguration<ICommand> CustomInsert
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the custom delete command.
        /// </summary>
        public IComponentConfiguration<ICommand> CustomDelete
        {
            get;
            set;
        }

        #endregion

        #region TableSchema

        /// <summary>
        /// Gets or sets the table schema uses for generate commands.
        /// </summary>
        public Table TableSchema
        {
            get;
            set;
        }

        #endregion

        /// <summary>
        /// Creates the command set.
        /// </summary>
        public ICommandSet CreateComponent(IOverStoreContext context)
        {
            if (context == null)
                throw new ArgumentNullException("context");
            if (TableSchema == null)
                throw this.ConfigurationException("TableScema is missing.");
            if (context.DataStorage.SqlFormatting == null)
                throw this.ConfigurationException("Current data storage does not support SQL command generation.");
            var sc = StringComparer.OrdinalIgnoreCase;
            if (!String.IsNullOrEmpty(TableSchema.TypeDiscriminatorColumnName) &&
                !TableSchema.Columns.Any(c => sc.Equals(c.Name, TableSchema.TypeDiscriminatorColumnName)))
                throw this.ConfigurationException("Type descriminator column name set but is not found in table schema columns.");

            var result = new Runtime.ObjectRepository.CommandSet.CommandSet
                             {
                                 SelectCommand =
                                     CustomSelect != null
                                         ? CustomSelect.CreateComponent(context)
                                         : CreateSelectCommand(context.Logger, context.DataStorage.SqlFormatting),
                                 InsertCommand =
                                     CustomInsert != null
                                         ? CustomInsert.CreateComponent(context)
                                         : CreateInsertCommand(context.Logger, context.DataStorage.SqlFormatting)
                             };

            if (CustomUpdate != null)
                result.UpdateCommand =
                    CustomUpdate.CreateComponent(context);
            else
                result.UpdateCommand = CreateUpdateCommand(context.Logger, context.DataStorage.SqlFormatting);

            if (CustomDelete != null)
                result.DeleteCommand =
                    CustomDelete.CreateComponent(context);
            else if (PerformDeleteAsUpdate)
                result.DeleteCommand = CreateUpdateCommand(context.Logger, context.DataStorage.SqlFormatting);
            else
                result.DeleteCommand = CreateDeleteCommand(context.Logger, context.DataStorage.SqlFormatting);

            return result;
        }

        /// <summary>
        /// Perform component validation. Method should validate only
        /// component configuration itself, not sub components.
        /// If configuration is valid, null result should returns.
        /// </summary>
        /// <returns>
        ///     <see cref="T:OverStore.Configuration.Validation.ValidationResult"/> describes errors found in component configuration.
        /// </returns>
        public ValidationResult Validate()
        {
            var discriminatorColumnExsits = TableSchema != null &&
                                              !String.IsNullOrEmpty(TableSchema.TypeDiscriminatorColumnName) &&
                                              !TableSchema.Columns.Any(c => StringComparer.OrdinalIgnoreCase.Equals(c.Name, TableSchema.TypeDiscriminatorColumnName ?? String.Empty));
            return new ValidationResult(this)
                .AddError(TableSchema == null, "Table Schema is missing")
                .AddError(TableSchema != null && !TableSchema.Columns.Any(c => c.ColumnType == ColumnType.PrimaryKey), "No Primary keys defined in TableSchema")
                .AddError(discriminatorColumnExsits, "Type Discriminator Column {0} is not found in table {1}", TableSchema.TypeDiscriminatorColumnName, TableSchema.Name)
                .ValidateToNestedResult(CustomDelete)
                .ValidateToNestedResult(CustomInsert)
                .ValidateToNestedResult(CustomSelect)
                .ValidateToNestedResult(CustomUpdate);
        }

        /// <summary>
        /// Creates new <see cref="AdoNetParameter"/> instance using specified <see cref="ColumnBase"/>.
        /// </summary>
        private AdoNetParameter ParameterFromColumn(ColumnBase column, string name, SqlFormatting formatting)
        {
            Debug.Assert(column != null, "column != null");
            Debug.Assert(name != null, "name != null");
            Debug.Assert(formatting != null, "formatting != null");
            var result = new AdoNetParameter
                             {
                                 AllowNull = column.AllowNull,
                                 DataType = column.OverrideDatabaseType ?? formatting.ToDatabaseType(column.ClrType),
                                 Direction = ParameterDirection.Input,
                                 Name = name,
                                 ErrorMessage = column.ErrorMessage
                             };
            return result;
        }

        private AdoNetCommandBase CreateSelectCommand(ILogger logger, SqlFormatting formatting)
        {
            string sql = TableSchema.CreateSelectSingleStatement(formatting);

            return new AdoNetCommand(
                            sql,
                            CommandType.Text,
                            TableSchema.KeyColumns().Select(c => ParameterFromColumn(c, c.OriginalIdentifierName, formatting)),
                            AdoCommandResultType.DataReaderResultSet,
                            logger);
        }

        private AdoNetCommandBase CreateUpdateCommand(ILogger logger, SqlFormatting formatting)
        {
            string sql = TableSchema.CreateUpdateStatement(formatting);

            return new AdoNetCommand(
                            sql,
                            CommandType.Text,
                            TableSchema.Columns
                                       .Select(c => ParameterFromColumn(c, c.ResultSetName, formatting))
                                       .Concat(TableSchema.KeyColumns()
                                                          .Select(c => ParameterFromColumn(c, c.OriginalIdentifierName, formatting))),
                            AdoCommandResultType.OutputParameters,
                            logger);
        }

        private AdoNetCommandBase CreateInsertCommand(ILogger logger, SqlFormatting formatting)
        {
            if (TableSchema.KeyColumns().Count() == 1 &&
                TableSchema.KeyColumns().First().IsDatabaseGenerated)
            {

                if (formatting.SupportsCompositeCommands)
                {
                    string sql = TableSchema.CreateInsertStatement(formatting) + ";" + Environment.NewLine +
                                 TableSchema.CreateSelectLastIdentity(formatting);
                    return new AdoNetCommand(
                        sql,
                        CommandType.Text,
                        TableSchema.Columns.Select(c => ParameterFromColumn(c, c.ResultSetName, formatting)).Concat(TableSchema.KeyColumns().Select(c => ParameterFromColumn(c, c.OriginalIdentifierName, formatting))).ToList(),
                        AdoCommandResultType.OutputParameters,
                        logger);
                }
                else
                {
                    var insertSql = TableSchema.CreateInsertStatement(formatting);
                    var insertCommand = new AdoNetCommand(
                                        insertSql,
                                        CommandType.Text,
                                        TableSchema.Columns
                                                   .Select(c => ParameterFromColumn(c, c.ResultSetName, formatting)).Concat(
                                                                TableSchema.KeyColumns()
                                                                           .Select(c =>
                                                                ParameterFromColumn(c, c.OriginalIdentifierName, formatting))).ToList(),
                                        AdoCommandResultType.OutputParameters,
                                        logger);

                    var lastIdCommand = new AdoNetCommand(
                            TableSchema.CreateSelectLastIdentity(formatting),
                            CommandType.Text,
                            Enumerable.Empty<AdoNetParameter>(),
                            AdoCommandResultType.DataReaderResultSet,
                            logger);
                    return new AdoNetScript(insertCommand, lastIdCommand, ResultComposition.MergePreferSecond);
                }
            }
            else
            {
                string sql = TableSchema.CreateInsertStatement(formatting);
                return new AdoNetCommand(
                    sql,
                    CommandType.Text,
                    TableSchema.Columns.Select(c => ParameterFromColumn(c, c.ResultSetName, formatting)).Concat(
                        TableSchema.KeyColumns().Select(c => ParameterFromColumn(c, c.OriginalIdentifierName, formatting))).ToList(),
                    AdoCommandResultType.OutputParameters,
                    logger);
            }
        }

        private AdoNetCommandBase CreateDeleteCommand(ILogger logger, SqlFormatting formatting)
        {
            string sql = TableSchema.CreateDeleteStatement(formatting);
            return new AdoNetCommand(
                    sql,
                    CommandType.Text,
                    TableSchema.KeyColumns()
                               .Select(c => ParameterFromColumn(c, c.OriginalIdentifierName, formatting))
                               .Concat(TableSchema.Columns
                                                  .Where(c => c.ColumnType == ColumnType.RowVersion)
                                                  .Select(c => ParameterFromColumn(c, c.ResultSetName, formatting))),
                    AdoCommandResultType.OutputParameters,
                    logger);
        }
    }
}